Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

26장. 임베딩과 RAG, 내 문서로 답하게 하기

이 장의 목표 회사 위키, 사내 매뉴얼, 회의록 등 내 문서를 AI가 참고해서 답하게 만드는 기술, RAG 의 원리를 끝까지 이해합니다.


26.1 왜 RAG 인가

문제 상황.

"우리 회사의 휴가 정책 알려줘"
        ↓
모델: "일반적으로 회사들은 ..." (헛소리)

모델은 우리 회사 내부 문서를 본 적이 없습니다.

해결책 두 가지.

방법한 줄
파인튜닝 (32장)모델 자체를 다시 학습
RAG답할 때마다 필요한 문서를 찾아서 같이 줌

RAG가 압도적으로 흔한 이유:

  • 문서가 자주 바뀌어도 즉시 반영
  • 모델 학습 안 함 (시간·비용·하드웨어 절약)
  • 출처 명시 가능
  • 작은 모델로도 동작

26.2 RAG의 큰 그림

[사용자 질문]
       │
       ▼
[질문을 벡터로 변환]   ← 임베딩 모델
       │
       ▼
[벡터 DB에서 비슷한 문서 찾기]
       │
       ▼
[관련 문서 + 질문을 LLM에 같이 전달]
       │
       ▼
[LLM이 그 문서 보면서 답]

이게 다입니다.


26.3 임베딩 — 의미를 숫자로

문장을 벡터 로 바꾸는 모델 (9장).

"휴가 정책 알려줘"
        ↓
[0.12, -0.45, 0.88, ..., 0.03]

비슷한 의미는 가까운 벡터 가 됩니다.

"휴가 정책 알려줘"      → [0.12, -0.45, ...]
"휴가는 어떻게 신청해?"  → [0.13, -0.41, ...]  (가까움)
"점심 메뉴 추천해"      → [-0.55, 0.66, ...] (멀음)

벡터 간 거리 또는 코사인 유사도 로 가까움을 잼.


26.4 대표 임베딩 모델

모델특징
BAAI/bge-m3다국어, 가장 인기
BAAI/bge-large-en영어 강함
intfloat/e5-mistral-7b큰 임베딩, 품질↑
nomic-embed-text빠름·작음
jinaai/jina-embeddings-v3다국어, 긴 컨텍스트

대부분 수백 MB ~ 2GB. LLM보다 훨씬 가볍습니다.

Ollama로 받기:

$ ollama pull bge-m3
$ ollama pull nomic-embed-text

26.5 임베딩 API

Ollama:

$ curl http://localhost:11434/api/embeddings -d '{
  "model": "bge-m3",
  "prompt": "휴가 정책 알려줘"
}'
{
  "embedding": [0.123, -0.456, 0.789, ...]
}

OpenAI 호환 형식:

$ curl http://localhost:11434/v1/embeddings -d '{
  "model": "bge-m3",
  "input": "휴가 정책 알려줘"
}'

26.6 청킹(Chunking) — 문서를 잘게 자르기

큰 문서를 통째로 넣으면

  • 임베딩이 부정확해지고
  • 컨텍스트가 폭주합니다

잘게 잘라서 각 조각의 벡터를 저장.

[원본 문서]
"인사 규정 ... (5,000자) ..."

       ↓ 청킹 (예: 500자 단위)

[조각 1] "인사 규정 ... 휴가는 ..."
[조각 2] "휴가 신청은 ... "
[조각 3] "병가는 ..."
...

청크 크기:

  • 너무 작음(100자) → 맥락 부족
  • 너무 큼(2000자) → 한 조각에 여러 주제 섞임
  • 권장: 300~800자, 약간 겹치게(overlap)

26.7 벡터 DB — 청크를 저장·검색

수만 개 청크 벡터를 어떻게 빨리 검색?

벡터 DB 가 해줍니다 (27장에서 본격).

작은 프로젝트면 그냥 SQLite/JSON도 OK.

이름      | 청크 텍스트  | 벡터
---------+-----------+--------------
chunk_001 | "휴가는 ..." | [0.12, ...]
chunk_002 | "병가는 ..." | [-0.34, ...]
...

질문이 오면:

  1. 질문도 임베딩 → 벡터
  2. DB에서 가장 가까운 N개 청크 찾기
  3. 그 청크들을 LLM 프롬프트에 첨부

26.8 RAG 프롬프트 패턴

청크를 찾은 다음 LLM에게 줄 프롬프트.

[시스템]
다음 "근거"만 보고 한국어로 답하세요.
근거에 없는 내용은 "확인 필요"라고 답하세요.

[근거]
[1] (chunk_023) "정직원의 연차는 1년 만근 시 15일이며..."
[2] (chunk_047) "휴가 신청은 사내 그룹웨어에서..."
[3] (chunk_005) "연차 사용은 부서장 승인 후..."

[질문]
정직원 연차는 몇 일이야?

[답변]

이렇게 주면 모델은 거의 환각 없이 답합니다.

핵심 규칙: “근거에 없으면 모른다고 답해” 한 줄이 환각을 막는 가장 큰 장벽.


26.9 가장 작은 RAG 코드 (Python)

from openai import OpenAI
import numpy as np

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

# 1. 문서 청크
docs = [
    "정직원의 연차는 1년 만근 시 15일이며...",
    "휴가 신청은 사내 그룹웨어에서...",
    "병가는 진단서가 필요하며...",
    "사내 식사 보조비는...",
]

# 2. 모든 청크 임베딩
def embed(text):
    r = client.embeddings.create(model="bge-m3", input=text)
    return np.array(r.data[0].embedding)

doc_vecs = [embed(d) for d in docs]

# 3. 검색
def search(query, k=2):
    qv = embed(query)
    sims = [np.dot(qv, dv) / (np.linalg.norm(qv) * np.linalg.norm(dv)) for dv in doc_vecs]
    idx = np.argsort(sims)[::-1][:k]
    return [docs[i] for i in idx]

# 4. RAG 답변
def rag_answer(query):
    chunks = search(query)
    context = "\n".join(f"[{i+1}] {c}" for i, c in enumerate(chunks))
    messages = [
        {"role": "system", "content": "다음 근거만 보고 한국어로 답해. 없으면 '확인 필요'."},
        {"role": "user",   "content": f"[근거]\n{context}\n\n[질문]\n{query}"},
    ]
    resp = client.chat.completions.create(model="qwen3:8b", messages=messages)
    return resp.choices[0].message.content

print(rag_answer("정직원 연차는?"))

이 60줄이 RAG의 본질입니다.


26.10 RAG의 자주 만나는 함정

① 검색이 정확하지 않음

  • 청크가 너무 큼 → 잘게 쪼개기
  • 임베딩이 약함 → bge-m3 같은 다국어 강한 모델로
  • 질문이 모호함 → Hybrid Search (키워드 + 벡터) 병행

② 모델이 근거 무시하고 학습 지식으로 답함

  • 시스템 프롬프트를 더 강하게: “근거에 명시되지 않은 내용은 절대 추가하지 않음”
  • 작은 모델일수록 자주 발생 → 14B 이상 권장

③ 같은 청크가 여러 번 반환됨

  • 중복 제거 후 retrieval

④ “확인 필요“가 너무 자주

  • top-k 를 2 → 4 정도로 늘림
  • 청크 크기 조정

26.11 RAG 품질 끌어올리기 — 다음 단계

기본 RAG가 동작하기 시작하면 다음 단계로:

기법설명
Reranker1차 검색 결과를 reranker 모델로 재정렬 (9장)
Hybrid Search벡터 + BM25 키워드 검색 병합
Query Rewriting질문을 검색 친화적으로 LLM이 다시 씀
Multi-hop한 번 검색 → 부족하면 추가 검색
Citation답변에 [1] [2] 같은 출처 표시

이 중 가장 효과 큰 건 Reranker. 다음 장에서 실제 도구와 함께 봅니다.


이 장에서 기억할 한 가지

RAG의 본질:

  1. 문서를 청크로 쪼개고 임베딩으로 저장
  2. 질문이 오면 비슷한 청크를 찾아서
  3. LLM에 “이 근거만 보고 답해” 라고 함께 전달

이게 사내 챗봇·문서 챗봇의 거의 모든 모습입니다.


손으로 해볼 것

1. 임베딩 모델 받기

$ ollama pull bge-m3

2. 임베딩 직접 호출

$ curl http://localhost:11434/v1/embeddings -d '{
  "model": "bge-m3",
  "input": "휴가 정책 알려줘"
}' | jq '.data[0].embedding | length'

벡터의 차원이 출력됩니다. (보통 1024)

3. 26.9 절 코드 실행

가상환경에 numpyopenai 설치 후 26.9 의 60줄 코드를 그대로 실행.

같은 질문을 RAG 없이 LLM 단독으로 시키고 응답 차이를 비교하세요.


다음 장에서는 진짜 벡터 DB — Chroma, Qdrant 같은 도구를 봅니다.

수천~수만 문서로 가도 무너지지 않는 구조가 보입니다.